<?php

class App_Source_Groups_Mapper extends Mozg_Mapper_Data
{

	private $_allData = array();
	private $_allDataRel = array();
	private $_isDataLoaded = false;

	private $_source;

	protected $_entityPath = 'entity/group';

	public function __construct()
	{
		$this->_source = Mozg_Source::get('default');
	}

	private function User()
	{
    	return Mozg::User();
	}

	private function CleanCache()
	{
		$this->_allData = array();
		$this->_allDataRel = array();
		$this->_isDataLoaded = false;
	}

    private function GetAllData()
    {
    	if ($this->_isDataLoaded) return;
    	$this->CleanCache();

        $stmt = $this->_source->PrepareStmt("SELECT
							                   " .$this->_source->Table('groups')->GetSqlMap(). "
							                FROM %%user_groups%%
							                ORDER BY parentId, id");

		$entityConfig = Mozg_Property_Config::Get($this->_entityPath);

        foreach ($stmt as $row)
        {
        	$obj = Mozg_Entity::Get($entityConfig, $row->ExportData());
			$id = $obj->GetId();

        	$this->_allData[$id] = $obj;
            $this->_allDataRel[$obj->GetParentId()][] = $id;
        }

        //добавляем группу суперюзера
        $superGroup = Mozg_Cfg::Get('global/superuser')->GetGroup();
		$this->_allData[$superGroup->GetId()] = $superGroup;

        $this->_isDataLoaded = true;
    }

    public function IsSuperUserGroup($groupId)
    {
		return Mozg_Cfg::Get('global/superuser')->GetGroup()->GetId() == $groupId;
    }

    protected function FilterData(array $groups, $ids)
    {
    	if (!is_array($ids)) $ids = array($ids);
    	return array_intersect_key($groups, array_flip($ids));
    }

	public function GetList($filter = null, Mozg_Paginator $paginator = null)
	{
		return $this->Find($filter, $paginator);
	}

	public function GetOne($filter = null)
	{
		return current($this->Find($filter, new Mozg_Paginator(1,1)));
	}

	protected function Find($filter = null, Mozg_Paginator $paginator = null, $fields = array())
	{
		$this->GetAllData();

		if (!($filter instanceof Mozg_Property_Params)) $filter = $this->GetFilter($filter);

		$groups = $this->_allData;

		if ($filter->HasSet('id'))
		{
			$groups = $this->FilterData($groups, $filter->GetId());
		}

		if ($filter->HasSet('parentId'))
		{
			$ids = $this->GetSubGroupsIds($filter->GetParentId(), $filter->GetLevels());
			$groups = $this->FilterData($groups, $ids);
		}

		if ($filter->HasSet('pathId'))
		{
			$ids = $this->GetParentGroupsIds($filter->GetPathId());
			$groups = $this->FilterData($groups, $ids);
		}

		$fGroups = array();
		foreach ($groups as $group)
		{
			/*if ((!$filter->HasSet('active')
				|| ($filter->GetActive() && $node->GetActive())
				|| (!$filter->GetActive() && !$node->GetActive()))
				&&
				(!$filter->HasSet('blocked')
				|| ($filter->GetBlocked() && $node->GetBlocked())
				|| (!$filter->GetBlocked() && !$node->GetBlocked())))
			{*/
				$fGroups[$group->GetId()] = clone $group;
			//}
		}

        return $fGroups;
	}

    protected function GetSubGroupsIds($groupIds, $levels = 0)
    {
        $levels = (int)$levels;
        if ($levels < 0) return array();
        $levels--;

    	$this->GetAllData();

    	if (!is_array($groupIds)) $groupIds = array($groupIds);

        $groups = array();
        while (list (, $id) = each($groupIds))
        {
        	if (isset($this->_allDataRel[$id]))
        	{
        		$groups = array_merge($groups, $this->_allDataRel[$id]);
        		if ($level >=0) $groups = array_merge($groups, $this->GetSubGroupsIds($this->_allDataRel[$id], $level));
        	}
        }

        return array_unique($groups);
    }

    protected function GetParentGroupsIds($groupId)
    {
    	$this->GetAllData();

    	$parentGroupsIds = array();
    	while (isset($this->_allData[$groupId])
    			&& ($group = $this->_allData[$groupId])
    			&& ($groupId = $group->GetParentId()))
    	{
    		$parentGroupsIds[] = $groupId;
    	}

		return $parentGroupsIds;
    }

    public function Save(Mozg_Property_Params $obj)
    {
    	$this->_source->LockTable(array('%%user_groups%%', '%%group_access%%'));
		$this->CleanCache();

    	if ($obj->Validator()->Validate())
    	{
	    	$this->Prepare($obj);

			if ($obj->GetId())
			{
				$filter = $this->GetFilter();
				$filter->SetId($obj->GetId());
				$status = $this->Update($obj, $filter);
			}
			else
			{
				$status = $this->Insert($obj);
			}
    	}
    	else
		{
			$status = false;
		}

		$this->_source->UnLockTable();

		return $status;
    }

    protected function Insert(Mozg_Property_Params $obj)
    {
		$fields = $this->_source->Table('groups')->GetSqlInsert($obj);
		$num = $this->_source->exec("INSERT INTO %%user_groups%%(" .$fields['fields']. ") VALUES (" .$fields['values']. ")");

    	if ($num)
    	{
    		$obj->SetId($this->_source->lastInsertID());
    		$this->CleanCache();

			return true;
    	}

    	return false;
    }

    protected function Update(Mozg_Property_Params $obj, $filter = null)
    {
		$objects = $this->GetList($filter);
		if (sizeof($object) > 1) throw new Mozg_Exception_System('Groups mass update currently unsupported');

		$updRows = array();
		foreach ($objects as $object)
		{
			$updateStr = $this->_source->Table('groups')->GetSqlUpdate($obj);
			if (!strlen($updateStr)) continue;

			$num = (bool)$this->_source->exec("update %%user_groups%%
												       set
													        " .$updateStr. "
												      where id='" .(int)$object->GetId(). "'
												     ");
	    	if ($num)
	    	{
				$updRows[$object->GetId()] = in_array($obj->GetParentId(), $this->GetParentGroupsIds($object->GetId()));
	    	}
		}

		$this->CleanCache();
		foreach ($updRows as $groupId => $isInParentGroups)
		{
		    if (!$isInParentGroups)
    		{
    			$this->NormilizeGroupsAccess($groupId);
    		}
		}

    	return sizeof($updRows);
    }

	protected function Prepare(Mozg_Property_Params $obj)
	{
        $availableGroups = $this->User()->GetAvailableGroups();

        if ($obj->GetId() && !in_array($obj->GetId(), $availableGroups))
        {
	        $obj->SetId(0);
        }
	}

    public function Delete($filter = null)
    {
        $items = $this->GetList($filter);

        foreach ($items as $item)
        {
	        if (!in_array($item->GetId(), $this->User()->GetAvailableGroups())) continue;

	        $num = (bool)$this->_source->exec("DELETE
				        								ug, ga
				        							FROM %%user_groups%% ug
				        									left join %%group_access%% ga on(ug.id=ga.group_id)
				        							WHERE
				        								ug.id='" .(int)$item->GetId(). "'");
	        if ($num)
	        {
	            $this->_source->exec("UPDATE
	            								%%user_groups%%
	            							SET
	            								parent_id='" . $item->GetParentId() . "'
	            							WHERE
	            								parent_id='" .$item->GetId(). "'");

		        $updRows++;
	        }
        }

		if ($updRows)
		{
			$this->CleanCache();
		}

        return $updRows;
    }

    const maxActionId = 256;

    /**
		возвращает двоичный код для id экшена
     */
    /**
     *
     * @param $shift - $ctrlId
     * @param $num - $actionId
     * @return binary string
     */
    public function CreateBinCode($shift, $num)
    {
    	$num = (int)$num;
        $shift = (int)$shift;
    	if (!$num || !$shift) return "\0";
        if ($num > self::maxActionId) throw new Mozg_Exception_System('Action Id is over limit: ' .self::maxActionId);

        $num --;
        $nullBytes = floor($num / 8) + ($shift - 1) * self::maxActionId / 8;
        $number = pow(2, $num % 8);
        return pack('@' . $nullBytes . 'C', $number);
    }

    private function NormilizeGroupsAccess($groupId, array $groupAccess = null)
    {
    	$addGroups = array();
    	if (!$this->IsSuperUserGroup($groupId))
    	{
	        $group = $this->GetOne(array('id' => $groupId));
	        if (!$group) return false;

	        if (!$this->IsSuperUserGroup($group->GetParentId()))
	        {
				$addGroups[] = $group->GetParentId();
	        }

	        $addGroups[] = $group->GetId();
    	}

        $ga = array_merge($addGroups, $this->GetSubGroupsIds($groupId, 100));
        if (!sizeof($ga)) return true;

        $sgAccess = $this->GetGroupsAccess($ga);
		if ($groupAccess !== null)
		{
        	$sgAccess[$groupId] = $groupAccess;
		}

        $sql = '';
        while (list ($gid, $v) = each($sgAccess))
        {
        	$curGroup = $this->GetOne(array('id' => $gid));

        	$parentGroupAccess = isset($sgAccess[$curGroup->GetParentId()]) ? $sgAccess[$curGroup->GetParentId()] : false;

            while (list($moduleId, $mAccess) = each($v))
            {
				if ($parentGroupAccess !== false)
				{
					if (isset($parentGroupAccess[$moduleId]))
					{
						$mAccess &= $parentGroupAccess[$moduleId];
					}
					else
					{
						$mAccess = $this->CreateBinCode(0, 0);
					}
				}

				$sgAccess[$gid][$moduleId] = $mAccess;

				$sql .= "(" .$moduleId. "," .$gid. "," .$this->_source->quote($mAccess). "),";
            }
        }

        if (strlen($sql))
        {
            $num = (bool)$this->_source->exec("REPLACE %%group_access%%(module_id, group_id, maccess)
							            values
							            	" . trim($sql, ',') . "
							        ");

            if ($num)
            {
	            $this->_source->exec("UPDATE %%user_groups%%
								            SET
								            	update_time = NOW()
								            WHERE
								            	id IN (" .(implode(',', $addGroups)). ")
								        ");
            }
        }

        return true;
    }

    public function GetGroupsAccess($groupIds, $moduleId = false)
    {
        if (!$groupIds) return array();
        if (!is_array($groupIds)) $groupIds = array($groupIds);

        // заполняем массив $groupAccess таким образом, чтобы сохранить порядок элементов в массиве
        $groupAccess = array_fill_keys($groupIds, array());
        $stmt = $this->_source->query("SELECT
							                   module_id moduleId,
							                   maccess mAccess,
							                   group_id groupId
						                 FROM %%group_access%%
						                 WHERE
						                 	group_id in ('" . implode("','", $groupIds) . "')
						               		" .($moduleId ? " and module_id='" .(int)$moduleId . "'" : ""));

        foreach ($stmt as $row)
        {
        	//исключаем контроллеры, к которым нет доступа
            if (!strlen(trim($row['mAccess'], "\0"))) continue;
            $groupAccess[$row['groupId']][$row['moduleId']] = $row['mAccess'];
        }

        return $groupAccess;
    }

    private function GetMergedGroupAccess($groupIds)
    {
        $groupAccess = $this->GetGroupsAccess($groupIds);
        $resultAccess = array();
        while (list(, $access) = each($groupAccess))
        {
        	while (list($moduleId, $mAccess) = each($access))
            {
	            if (isset($resultAccess[$moduleId]))
	            {
	                $resultAccess[$moduleId] |= $mAccess;
	            }
	            else
	            {
	                $resultAccess[$moduleId] = $mAccess;
	            }
            }
        }

        return $resultAccess;
    }

    protected $_moduleBoundary = '###########################';
    protected $_accessBoundary = '%==%';

    public function GetMergedGroupAccessFromCache($groupIds)
    {
        $accessFilename = $this->GetCacheFileName('access', (array)$groupIds);
        if (is_file($accessFilename) && (filemtime($accessFilename) > $this->GetGroupsUpdateTime((array)$groupIds)))
        {
        	$groupAccess = array();

        	$accessData = explode($this->_moduleBoundary, file_get_contents($accessFilename));
        	foreach ($accessData as $moduleAccess)
        	{
        		list($moduleId, $mAccess) = explode($this->_accessBoundary, $moduleAccess);
        		$groupAccess[$moduleId] = $mAccess;
        	}

        	return $groupAccess;
        }

        $groupAccess = $this->GetMergedGroupAccess($groupIds);
        //$exportAccess = '<?php return ' . var_export($groupAccess, TRUE) . ';';
        if (Mozg::Lib('dir')->Create(_CACHE_CONFIG_PATH))
        {
        	$exportAccess = array();
        	foreach ($groupAccess as $moduleId => $mAccess)
        	{
        		$exportAccess[] = $moduleId.$this->_accessBoundary.$mAccess;
        	}

        	file_put_contents($accessFilename, implode($this->_moduleBoundary, $exportAccess));
        	@chmod($accessFilename, _CHMOD_FILE);
        }

        return $groupAccess;
    }

    /**
		возвращает имя файла кэша, по списку групп и префиксу файла
     */
    public function GetCacheFileName($prefix, array $groupIds)
    {
    	sort($groupIds);
        return _CACHE_CONFIG_PATH . '_' .$prefix. '_' .md5(implode(':', $groupIds));
    }

    /**
		возвращает общее время последнего обновления групп
     */
    public function GetGroupsUpdateTime(array $groupIds)
    {
    	if (!sizeof($groupIds)) return 0;

    	$groupsUpdateTime = 0;
    	while (list(, $id) = each($groupIds))
    	{
			$group = $this->GetOne(array('id' => $id));
			if (!$group) continue;
			if ($groupsUpdateTime < $group->GetUpdateTime()) $groupsUpdateTime = $group->GetUpdateTime();
    	}
        return $groupsUpdateTime;
    }

    public function CheckAccess($accessStr, $ctrlId = false, $actionId = false)
    {
    	if ($ctrlId && $actionId)
    	{
			return (bool)strlen(trim((string)$accessStr & $this->CreateBinCode($ctrlId, $actionId), "\0"));
    	}
    	else if ($ctrlId && !$actionId)
    	{
    		$nullBytesNum = self::maxActionId / 8;
    		return (bool)strlen(trim(substr($accessStr, ($ctrlId - 1) * $nullBytesNum, $nullBytesNum), "\0"));
    	}
    	else if (!$ctrlId && !$actionId)
    	{
    		return (bool)strlen(trim((string)$accessStr, "\0"));
    	}

    	throw new Mozg_Exception_System('Invalid params');
    }

    public function SaveGroupAccess($groupId, $moduleId, $ctrlId, array $actions)
    {
    	$this->_source->LockTable(array('%%user_groups%%', '%%group_access%%'));
		$this->CleanCache();

		try
		{
			if (!in_array($groupId, $this->User()->GetAvailableGroups())) throw new Mozg_Exception_System('Group not available');
	        $group = $this->GetOne(array('id' => $groupId));
	        if (!$group) throw new Mozg_Exception_System('Group not found');

	        $allActionIds = Mozg_Module::GetCtrlActionIds($moduleId, $ctrlId);

	        $actions = array_intersect($actions, $allActionIds);
	        $sAccess = $this->CreateBinCode(0, 0);

	        while (list (, $v) = each($actions))
	        {
	            $sAccess |= $this->CreateBinCode($ctrlId, $v);
	        }

	        $groupAccess = current($this->GetGroupsAccess($groupId));

	        if (isset($groupAccess[$moduleId])
	        	&& ($groupAccess[$moduleId] !== false)
	        	&& ($groupAccess[$moduleId] !== null))
	        {
	        	$nullBytesNum = self::maxActionId / 8;
	        	if (strlen($groupAccess[$moduleId]) >= ($ctrlId - 1) * $nullBytesNum)
	        	{
	        		$groupAccess[$moduleId] = substr_replace($groupAccess[$moduleId], str_repeat("\0", $nullBytesNum), ($ctrlId - 1) * $nullBytesNum, $nullBytesNum);
	        	}
	        }
	        else
	        {
	        	$groupAccess[$moduleId] = $this->CreateBinCode(0, 0);
	        }

	        $groupAccess[$moduleId] |= $sAccess;

			$status = $this->NormilizeGroupsAccess($groupId, $groupAccess);
		}
		catch (Exception $e)
		{
			$status = false;
		}

		$this->_source->UnLockTable();

		return $status;
    }

}